const fs = require('fs') // 文件系统模块
const path = require('path') // 路径模块
const LRU = require('lru-cache') // LRU缓存模块
const express = require('express') // Express框架
const favicon = require('serve-favicon') // 网站图标中间件
const compression = require('compression') // 压缩中间件
const microcache = require('route-cache') // 微缓存中间件
const resolve = file => path.resolve(__dirname, file) // 解析路径
const { createBundleRenderer } = require('vue-server-renderer') // 创建服务器渲染器

const isProd = process.env.NODE_ENV === 'production' // 判断是否为生产环境
// 判断是否使用微缓存
const useMicroCache = process.env.MICRO_CACHE !== 'false'
// 服务器信息
const serverInfo =
  `express/${require('express/package.json').version} ` +
  `vue-server-renderer/${require('vue-server-renderer/package.json').version}`

const app = express()

// 创建渲染器函数
// bundle: 服务器端打包信息（JSON）
// options(可选): 包含客户端打包的清单信息（JSON） 和 用来生成html的模板文件
function createRenderer (bundle, options) {
  // https://github.com/vuejs/vue/blob/dev/packages/vue-server-renderer/README.md#why-use-bundlerenderer
  return createBundleRenderer(bundle, Object.assign(options, {
    // LRU缓存(感觉是(url -> html)的缓存，缓存那些服务端渲染生成的html页面，key是url路径)
    cache: LRU({
      max: 1000,
      maxAge: 1000 * 60 * 15
    }),
    // 当vue-server-renderer是通过npm链接的时候需要指定basedir
    basedir: resolve('./dist'),
    // 推荐为了性能设置为false
    runInNewContext: false
  }))
}

let renderer
let readyPromise
const templatePath = resolve('./src/index.template.html')
if (isProd) {
  // 在生产环境下：使用模板和构建好的服务器bundle创建服务器渲染器
  // 服务器bundle由vue-ssr-webpack-plugin生成
  const template = fs.readFileSync(templatePath, 'utf-8') // 读取模板文件
  const bundle = require('./dist/vue-ssr-server-bundle.json') // 读取服务器bundle
  // 客户端清单是可选的，但它允许渲染器自动推断出预加载/预取链接，并在渲染期间直接添加<script>标签，避免了瀑布请求。
  const clientManifest = require('./dist/vue-ssr-client-manifest.json') // 读取客户端清单
  renderer = createRenderer(bundle, {
    template,
    clientManifest
  })
} else {
  // 在开发环境下：设置开发服务器并进行热重载，当bundle/index模板更新时创建新的渲染器
  // readyPromise状态只会在第一次初始化好templatePath/bundle/clientManifest后才会变为resolve
  readyPromise = require('./build/setup-dev-server')(
    app,
    templatePath,
    (bundle, options) => {
      // template变化/客户端配置变化/服务器端配置变化时，该函数会重新执行，然后重新创建渲染器
      // 第二次开始，readyPromise的任务已经完成，后续的请求都会直接调用该函数创建新的渲染器
      renderer = createRenderer(bundle, options)
    }
  )
}

// 静态资源中间件
const serve = (path, cache) => express.static(resolve(path), {
  maxAge: cache && isProd ? 1000 * 60 * 60 * 24 * 30 : 0
})

// 使用压缩中间件
app.use(compression({ threshold: 0 }))
// 使用网站图标中间件
app.use(favicon('./public/logo-48.png'))
// 静态资源路由
app.use('/dist', serve('./dist', true))
app.use('/public', serve('./public', true))
app.use('/manifest.json', serve('./manifest.json', true))
app.use('/service-worker.js', serve('./dist/service-worker.js'))

// 由于此应用程序没有特定于用户的内容，每个页面都可以进行微缓存。
// 如果您的应用程序涉及特定于用户的内容，则需要根据其URL和标头实现自定义逻辑来确定请求是否可缓存。
// 1秒的微缓存。
app.use(microcache.cacheSeconds(1, req => useMicroCache && req.originalUrl))

// 渲染函数
function render (req, res) {
  console.log(req.url)
  const s = Date.now()

  res.setHeader("Content-Type", "text/html")
  res.setHeader("Server", serverInfo)

  const handleError = err => {
    if (err.url) {
      res.redirect(err.url)
    } else if(err.code === 404) {
      res.status(404).send('404 | Page Not Found')
    } else {
      // 渲染错误页面或重定向
      res.status(500).send('500 | Internal Server Error')
      console.error(`error during render : ${req.url}`)
      console.error(err.stack)
    }
  }
  // 很关键的上下文信息，包含了title和url，后续会插入服务端渲染好的store信息
  const context = {
    title: 'Vue HN 2.0', // 默认标题
    url: req.url
  }
  // renderer.renderToString调用的时候回去，以后会去调用entry-server.js导出的方法
  /* TODO: 最关键的步骤
    1. 前置条件：renderer在创建的时候已经拿到了服务端和客户端打包后的信息，即bundle和clientManifest，当然还有用户自定义的html template信息
    2. 把context信息传入服务端入口文件导出的入口函数，内部创建新的vue/router/store实例，router加载对应url，加载对应异步路由组件，加载完成后
       onready钩子执行，执行每一个涉及的路由组件中的asyncData方法，服务端预期数据，填充数据到对应的store的对应state处，然后路由组件内部用computed
       关联store以获得响应性，当然有些直接就是定义在data中，因为在组件实例初始化时，store数据已经初始化了
    3. 对于服务端而言，会给每个组件内部混入一个created hooks，在该钩子内部会设置context的title，最后会加载到html模板的title处，有利于seo优化
       所以我们可以知道，其实不管是服务端渲染还是客户端激活，都会执行组件的created钩子
    4. 最终各个组件的render函数会执行，生成虚拟dom，然后和客户端不同（客户端虚拟dom会转换为真实的dom节点），最后虚拟dom会被转换为HtmlString
    5. 通过客户端清单文件clientManifest在html模板的head中，把需要引入的客户端最终打包文件插入，自动分析以什么方式的script标签preload还是prefetch
       通过验证发现一些暂时没用到的异步路由组件的打包文件会被prefetch，急需的当前需要渲染的组件打包文件会被preload，当然主要还是客户端入口文件的打包文件
       用来激活客户端Vue
  */
  renderer.renderToString(context, (err, html) => {
    if (err) {
      return handleError(err)
    }
    res.send(html)
    if (!isProd) {
      console.log(`whole request: ${Date.now() - s}ms`)
    }
  })
}

// 路由处理
app.get('*', isProd ? render : (req, res) => {
  readyPromise.then(() => render(req, res))
})

// 启动服务器
const port = process.env.PORT || 9999
app.listen(port, () => {
  console.log(`server started at localhost:${port}`)
})
